<?php
abstract class ModulePayment
{
    /**
     * @var int
     */
    protected $userId;
    /**
     * Platform name.
     *
     * @var string
     */
    protected $platform;

    /**
     * Construction.
     *
     * @param int $userId
     * @param string $platform
     */
    public function __construct($userId, $platform)
    {
        $this->userId = $userId;
        $this->platform = $platform;
        // initialize application context
        $this->context = SzContextFactory::get();
    }

    /**
     * Create payment order, order status after this operation will be ModulePaymentConst::$STATUS_UNFINISHED.
     *
     * @param int $userId
     * @param int | array $config
     * @param string $platformToken default null
     * @param array $extra default array()
     * @param boolean $forceCreate default false
     * @throws SzException 30503
     * @return ModulePaymentOrderVo $order
     */
    public function createOrder($userId, $config, $platformToken = null, $extra = array(), $forceCreate = false)
    {
        $paymentSetting = $this->getPaymentSetting($config);

        /**
         * @var SzMySqlModel $orderModel
         * @var ModulePaymentOrderVoList $orderList
         * @var ModulePaymentOrderVo $lastOrder
         */
        $orderModel = SzPersister::get()->getModel(ModulePaymentConst::ORM_NAME_PAYMENT_ORDER);
        $orderList = SzPersister::get()->getVoList(null, ModulePaymentConst::ORM_NAME_PAYMENT_ORDER, $userId);

        $lastOrder = null;
        if (!$forceCreate) {
            if ($orderList->getListCount() > 0) {
                $orderData = $orderList->getList();
                $lastOrder = array_pop($orderData);
            }
        }

        if (!is_null($lastOrder)
            && $lastOrder->getStatus() == ModulePaymentConst::STATUS_INITIALIZED) {
            // this user has ordered once, and last order created is initialized status, means user not payed for it, we can reuse it
            $order = $lastOrder;
            $order->setPlatformToken($platformToken);
            $order->setPaymentDefId($paymentSetting['paymentDefId']);
            $order->setOrderTime(SzTime::getTime());
            $order->setExtra(json_encode($extra));
            $order->setPrice($paymentSetting['price']);

            list($affectedRows) = $orderModel->update($order);
            if ($affectedRows) {
                $orderList->setElement($order->getOrderId(), $order);
                $orderModel->setListOfVoCache(array($order), $orderList->getList());
            } else {
                throw new SzException(30503, 'createOrder', ModulePaymentConst::MODULE_NAME);
            }
        } else {
            $order = new ModulePaymentOrderVo(
                null, // orderId
                $userId, // userId
                $platformToken, // platformToken
                $paymentSetting['paymentDefId'], // paymentDefId
                SzTime::getTime(), // orderTime
                null, // completeTime
                ModulePaymentConst::STATUS_INITIALIZED, // status
                json_encode($extra), // extra
                $paymentSetting['price'], // price
                true
            );

            list($affectedRows, $orderId) = $orderModel->insert($order);
            if ($affectedRows) {
                $order->setOrderId($orderId);
                $order->removeInsertFlag();
                $orderList->setElement($orderId, $order);
                $orderModel->setListOfVoCache(array($order), $orderList->getList());
            } else {
                throw new SzException(30503, 'createOrder', ModulePaymentConst::MODULE_NAME);
            }
        }

        return $order;
    }

    /**
     * Get payment setting.
     * Since the config parameter could be paymentSetting itself, this utility function could help.
     *
     * @param int | array $config INT: $paymentDefId, ARRAY: $paymentSetting
     * @return array $paymentSetting
     */
    protected function getPaymentSetting($config)
    {
        if (!is_array($config)) { // config is: int type paymentDefId
            $paymentSetting = SzConfig::get()->loadAppConfig(ModulePaymentConst::CONFIG_PAYMENT, $config, true);
        } else { // config is: $paymentSetting itself
            $paymentSetting = $config;
        }

        return $paymentSetting;
    }

    /**
     * Entrance of all complete order & supply *.
     * <pre>
     * NOTE: In this logic process $order object not persisted yet, and not be set into SzPersister yet,
     * it shall be done in later logic.
     * </pre>
     *
     * @param int $userId
     * @param ModulePaymentOrderVo $order
     * @param array $paymentSetting
     * @param int $quantity
     * @throws SzException 30501
     * @return ModulePaymentOrderVo $order
     */
    protected function completeOrderAndSupplyResources($userId, $order, $paymentSetting, $quantity)
    {
        if ($order->getStatus() < ModulePaymentConst::STATUS_PAYED) { // update order status to payed
            $order->setStatus(ModulePaymentConst::STATUS_PAYED);
        }

        switch ($paymentSetting['type']) {
            case ModulePaymentConst::PAYMENT_TYPE_FIRST_PAY_TOKEN:
            case ModulePaymentConst::PAYMENT_TYPE_TOKEN:
                $order = $this->completeOrderAndSupplyToken($userId, $order, $paymentSetting, $quantity);
                break;
            case ModulePaymentConst::PAYMENT_TYPE_FIRST_PAY_MONEY:
            case ModulePaymentConst::PAYMENT_TYPE_MONEY:
                $order = $this->completeOrderAndSupplyMoney($userId, $order, $paymentSetting, $quantity);
                break;
            case ModulePaymentConst::PAYMENT_TYPE_PACK:
                $order = $this->completeOrderAndSupplyItems($userId, $order, $paymentSetting, $quantity);
                break;
            default:
                break;
        }

        return $order;
    }

    /**
     * Complete order and supply player token.
     *
     * @param int $userId
     * @param ModulePaymentOrderVo $order
     * @param array $paymentSetting
     * @param int $quantity
     * @throws SzException 30501
     * @return ModulePaymentOrderVo $order
     */
    protected function completeOrderAndSupplyToken($userId, $order, $paymentSetting, $quantity)
    {
        // add items
        if (!$order->getStatus() == ModulePaymentConst::STATUS_PAYED) {
            throw new SzException(30501, array($order->getStatus()), ModulePaymentConst::MODULE_NAME);
        }

        // add commodity value
        $addedCommodityValue = $order->getPrice() * ModulePaymentConst::SINGLE_COMMODITY_VALUE_OF_PAY;

        /** @var ModuleProfileVo $profile */
        $profile = SzPersister::get()->getVo($userId, ModuleProfileConst::ORM_NAME_PROFILE);
        ModuleProfile::get()->updateProfileToken($profile, $paymentSetting['amount'] * $quantity, true, $addedCommodityValue);
        SzPersister::get()->setVo($profile);

        // change order
        $order->setCompleteTime(SzTime::getTime());
        $order->setStatus(ModulePaymentConst::STATUS_COMPLETED);

        return $order;
    }

    /**
     * Complete order and supply player money.
     *
     * @param int $userId
     * @param ModulePaymentOrderVo $order
     * @param array $paymentSetting
     * @param int $quantity
     * @throws SzException 30501
     * @return ModulePaymentOrderVo $order
     */
    protected function completeOrderAndSupplyMoney($userId, $order, $paymentSetting, $quantity)
    {
        // add items
        if (!$order->getStatus() == ModulePaymentConst::STATUS_PAYED) {
            throw new SzException(30501, array($order->getStatus()), ModulePaymentConst::MODULE_NAME);
        }

        // add commodity value
        $addedCommodityValue = $order->getPrice() * ModulePaymentConst::SINGLE_COMMODITY_VALUE_OF_PAY;

        /** @var ModulePaymentVo $payment */
        $payment = SzPersister::get()->getVo(null, ModulePaymentConst::ORM_NAME_PAYMENT, $userId);
        $this->updateMoneyAmount($payment, $paymentSetting['amount'] * $quantity, $addedCommodityValue);
        SzPersister::get()->setVo($payment);

        // change order
        $order->setCompleteTime(SzTime::getTime());
        $order->setStatus(ModulePaymentConst::STATUS_COMPLETED);

        return $order;
    }

    /**
     * Complete order and supply player items.
     *
     * @param int $userId
     * @param ModulePaymentOrderVo $order
     * @param array $paymentSetting
     * @param int $quantity
     * @throws SzException 30501
     * @return ModulePaymentOrderVo $order
     */
    protected function completeOrderAndSupplyItems($userId, $order, $paymentSetting, $quantity)
    {
        // add items
        if (!$order->getStatus() == ModulePaymentConst::STATUS_PAYED) {
            throw new SzException(30501, array($order->getStatus()), ModulePaymentConst::MODULE_NAME);
        }

        // add commodity value
        $addedCommodityValue = $order->getPrice() * ModulePaymentConst::SINGLE_COMMODITY_VALUE_OF_PAY;

        /** @var ModuleItemVoList $itemList */
        $itemList = SzPersister::get()->getVoList($userId, ModuleItemConst::ORM_NAME_ITEM);
        $itemList->prepareForNextExceededItems();
        $itemList = ModuleItem::get()->addItemCount($userId, $paymentSetting['amount'] * $quantity, $itemList, $paymentSetting['packId'], null, array(), false, true, $addedCommodityValue);
        $order->setExceededItemList($itemList->getLastExceededItems());

        SzPersister::get()->setVoList($itemList);

        // change order
        $order->setCompleteTime(SzTime::getTime());
        $order->setStatus(ModulePaymentConst::STATUS_COMPLETED);

        return $order;
    }

    /**
     * Update player money.
     *
     * @param ModulePaymentVo $payment
     * @param int $money possible to be negative number, means minus money
     * @param int $commodityValue default 0，it shall always be positive number
     * @throws SzException 30500
     * @return ModulePaymentVo $payment
     */
    public function updateMoneyAmount($payment, $money, $commodityValue = 0)
    {
        if ($commodityValue > 0 && $money > 0) {
            // 增加money，并且价值大于0，默认为增加付费货币
            $isFreeMoney = false;
        } else {
            // 增加money，并且价值小于等于0，默认为增加免费货币
            // 扣除money，默认为扣除免费货币，免费货币不足，扣除付费货币，并计算扣除的价值
            $isFreeMoney = true;
            $commodityValue = 0;
        }

        $currentMoney = $payment->getMoney() + $money;

        // validate money enough or not
        if ($money < 0 && $currentMoney < 0) {
            throw new SzException(30500, array($money, $currentMoney), ModulePaymentConst::MODULE_NAME); // not enough money
        }

        if ($isFreeMoney || $money < 0) {
            if ($money < 0 && $payment->getBonusMoney() < abs($money)) {
                // means bonus token not enough to pay the amount, also need to minus purchased token
                $changedMoney = abs($payment->getBonusMoney() + $money);
                $changedCommodityValue = -$changedMoney * ($payment->getCommodityValue() / $payment->getPurchasedMoney());
                $payment->setBonusMoney(0);
                $payment->updatePurchasedMoney(-$changedMoney);
                $payment->updateCommodityValue($changedCommodityValue);
            } else {
                $payment->updateBonusMoney($money);
            }
        } else {
            // add token
            if ($isFreeMoney) {
                $payment->updateBonusMoney($money);
            } else {
                $payment->updatePurchasedMoney($money);
                $payment->updateCommodityValue($commodityValue);
            }
        }

        return $payment;
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* INTERFACES
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Complete payment order, order status after this operation will be ModulePaymentConst::$STATUS_UNFINISHED.
     * Since this functionality shall be the callback from the platform, each platform always has it's own way to provide various params,
     * we define no params here, the implementation logic shall parse the $_REQUEST by yourself.
     *
     * @return mixed
     */
    abstract public function completeOrder();
    /**
     * Reformat the return value after the process of complete order
     *
     * @param array $result
     * @return mixed
     */
    abstract public function completeOrderReturnValue($result);
    /**
     * Repair order, if order is paid & not completed, give user items and mark the order status.
     * It's possible for some platfrom without this functionality, just leave implementation an empty function.
     * @param int $userId
     * @param int $orderInfo order id or order token, which is used to retrieve the order record
     * @return ModulePaymentOrderVo $order
     */
    abstract public function repairOrder($userId, $orderInfo);
}